package math.mathExtended;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import tuples.Pair;
import variable.lazy.Lazy;
import variable.lazy.LazyValue;

/**
 *
 * @author Shimu
 * @date 4-Nov-2012
 */
public class Fraction {

    private final int NUMERATOR;

    private final int DENOMINATOR;

    private final double VALUE;

    /**
     * Instantiates a fraction with the given NUMERATOR and DENOMINATOR.
     * Reduces the fraction to the simplest form.
     * 
     * @param NUMERATOR
     * @param DENOMINATOR 
     */
    public Fraction(int numerator, int denominator) {
        Pair<Integer, Integer> result = this.simplify(numerator, denominator);
        this.NUMERATOR = result.getFirst();
        this.DENOMINATOR = result.getSecond();
        this.VALUE = this.NUMERATOR / (double) this.DENOMINATOR;
    }

    /**
     * @param decimal the decimal to be converted to a fraction
     * @param maxIter the maximum number of iterations before we exit
     * @param errorTolerance the minimum error before we exit
     */
    public Fraction(final double decimal, int maxIter, double errorTolerance) {
        Pair<Integer, Integer> result = this.decimalToFraction(decimal,
                maxIter, errorTolerance);
        this.NUMERATOR = result.getFirst();
        this.DENOMINATOR = result.getSecond();
        this.VALUE = this.NUMERATOR / (double) this.DENOMINATOR;
    }

    public Fraction(double decimal) {
        this(decimal, 10, 0.001); // 0.1 % Error tolerance
    }
    private static final Lazy<Pattern> FRACTION_PATTERN =
            new LazyValue<Pattern>() {

                @Override
                public Pattern initialize() {
                    return Pattern.compile("(-{0,1}\\d++)/(-{0,1}\\d++)");
                }
            };

    /**
     * 
     * @param str 
     * @throws IllegalArgumentException if the given String does not represent
     * a fraction
     */
    public Fraction(String str) {
        Matcher m = FRACTION_PATTERN.getValue().matcher(str.trim());
        final Pair<Integer, Integer> result;
        if (m.matches()) {
            result = this.simplify(Integer.parseInt(m.group(1)),
                    Integer.parseInt(m.group(2)));
        } else {
            try {
                result = this.decimalToFraction(Float.parseFloat(str),
                        10, 0.001);
            } catch (NumberFormatException ex) {
                throw new IllegalArgumentException("The given string does"
                        + " not represent a fraction! Must be a decimal number "
                        + "or a string of the form: integer/integer");
            }
        }
        this.NUMERATOR = result.getFirst();
        this.DENOMINATOR = result.getSecond();
        this.VALUE = this.NUMERATOR / (double) this.DENOMINATOR;
    }

    private Pair<Integer, Integer> simplify(int numerator, int denominator) {
        final boolean negative = (numerator * denominator) < 0;
        numerator = Math.abs(numerator);
        denominator = Math.abs(denominator);
        int gcd = MathExtended.greatestCommonDivisor(numerator,
                denominator);
        if (gcd == 0) {
            gcd = 1;
        }
        
        numerator *= (negative ? -1 : 1); // Put the sign back
        
        return new Pair<Integer, Integer>(numerator / gcd, denominator / gcd);
    }

    private Pair<Integer, Integer> decimalToFraction(double decimal,
            int maxIter, double errorTolerance) {
        final boolean negative = decimal < 0;
        final int denominators[] = new int[maxIter + 1];

        decimal = Math.abs(decimal);
        double tempDecimal = decimal;
        // Compute all the denominators
        for (int i = 0; i < maxIter + 1; i++) {
            denominators[i] = (int) tempDecimal;
            tempDecimal = 1.0 / (tempDecimal - denominators[i]);
        }

        // Compute the i-th approximation
        double error = 1; // Set error to 100%        
        int numerator = 1, denominator = 1, temp;
        for (int last = 0; (last < maxIter)
                && (error > errorTolerance); last++) {
            numerator = 1;
            denominator = 1;
            temp = 0;

            for (int i = last; i >= 0; i--) {
                denominator = numerator;
                numerator = (numerator * denominators[i]) + temp;
                temp = denominator;
            }

            double value = (double) numerator / denominator;
            error = Math.abs((value - decimal) / decimal);
        }
        return this.simplify(numerator * (negative ? -1 : 1),
                denominator);
    }

    /**
     * Returns the decimal representation of this Fraction (which may 
     * lose accuracy).
     * @return 
     */
    public double getDecimalRepresentation() {
        return this.VALUE;
    }

    /**
     * Returns the NUMERATOR of this Fraction. Note this may not equal the
     * NUMERATOR given in the constructor as the fraction is reduced to
     * its simplest form.
     * @return the NUMERATOR
     */
    public int getNumerator() {
        return this.NUMERATOR;
    }

    /**
     * Returns the DENOMINATOR of this Fraction. Note this may not equal the
     * DENOMINATOR given in the constructor as the fraction is reduced to
     * its simplest form.
     * @return the DENOMINATOR
     */
    public int getDenominator() {
        return this.DENOMINATOR;
    }

    /**
     * Returns numerator/denominator
     * @return 
     */
    @Override
    public String toString() {
        return String.format("%d/%d", this.NUMERATOR, this.DENOMINATOR);
    }
}
